/*
 * DataBlock.java
 *
 * Created on July 16, 2007, 11:07 AM
 *
 *  Copyright (C) 2007 Akhmad Fathonih
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * Changelog:
 * 2007/10/31
 * - handle null compound value
 * 2007/9/22
 * - add flush method to explicitely write changes od property to bytesource
 * - fix getCompound method, add proper zero padding to compound bytes shorter than the requirend EndIndex - BeginIndex
 */

package com.gamatechno.anybinding.core;

import com.gamatechno.anybinding.util.Converter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import org.jibx.runtime.IUnmarshallingContext;

/**
 * 
 * @author Akhmad Fathonih (akhmadf@gmail.com)
 */
public class DataBlock implements IDataBlock {
    private static int GlobalCounter = 0;
    private int internalID = 0;
    private byte[] ByteSource = null;
    private byte[] cachedValue = null;
    private IDataBlock Parent;
    private java.lang.Object MarshallObj;
    private HashMap<String, DataBlock> Props;

    public int BeginIndex;
    public int EndIndex;
    public String MarshallType;
    public ArrayList<DataBlock> children;
    public String Alias;
    public String Description;
    public String MarshallSetupMethod;

    private boolean LoggingEnabled;

    private boolean cachedValueIsInitialized;
    
        
    /** Creates a new instance of DataBlock */
    public DataBlock(int start, int end, IDataBlock parent, java.lang.Object marshalObj) {
        BeginIndex = start;
        EndIndex = end;
        Parent = parent;
        this.updateByteSource();
        setMarshallObj(marshalObj);
    }
    
    private void updateByteSource() {
        this.log("About to update ByteSource");
        if (this.Parent != null) {
            byte[] tmp = this.Parent.value(false);
            if (tmp != null) {
                this.log("About to updateByteSource on dataBlock #" + this.internalID);
                this.log("Parent.value(): " + Converter.byteToHexString(tmp));
                this.log("ByteSource before update (bytes): " + ((ByteSource != null) && (ByteSource.length!=0)?Converter.byteToHexString(ByteSource): "null"));
                setByteSource(tmp);
                this.log("ByteSource after update: " + (ByteSource.length!=0?Converter.byteToHexString(ByteSource): "null"));
            } else {
                this.log("Parent.value is NULL. Aborting updateByteSource");
            }
        } else {
            this.log("Parent is NULL. Aborting updateByteSource");
        }
    }
    public void setBeginIndex(int idx) {
        BeginIndex = idx;
    }
    
    public void setEndIndex(int idx) {
        EndIndex = idx;
    }
    
    public void setByteSource(byte[] source) {
        this.log("Setting bytesource: " + source.length);
        this.ByteSource = source;
    }
    
    public void setCachedValue(byte[] data) {
        this.cachedValue = data;
        this.cachedValueIsInitialized = true;
    }
    
    public byte[] value() {
        boolean useCache = this.cachedValueIsInitialized /*) || (ByteSource == null)*/;
        this.log("Use cache: " + useCache);
        return value(useCache);
    }
    
    public byte[] value(boolean useCache) {
        byte[] result = null;
        
        if (!useCache) {
            this.updateByteSource();
            if (this.ByteSource != null) {
                if (this.ByteSource.length == 0)
                    return null;
                this.log("ByteSource: " + this.ByteSource.length);
                String tmp = Converter.byteToHexString(this.ByteSource);
                this.log("value: " + tmp);
                this.log("About to subByte ByteSource: " + BeginIndex + " <--> " + EndIndex + "(" + (EndIndex - BeginIndex) + " bytes)");
//                String res = tmp.substring(2*BeginIndex, 2*EndIndex-1);
//                this.log("Res: " + res);
                //byte[] bres = Converter.hexStringToByte(res);
                byte[] bres = Converter.subByte(this.ByteSource, BeginIndex, EndIndex - BeginIndex);
                this.log("res is " + bres.length + " bytes");
                result = bres;
                this.cachedValueIsInitialized = true;
                //return (new String(ByteSource)).substring(BeginIndex, EndIndex).getBytes();
            } else
                result = null;
            this.cachedValue = result;
        } else {
            result = this.cachedValue;
        }
        
        return result;
    }
    
    public void setup(String MarshallSetupMethod) {
        if (this.MarshallObj != null) {
            if (!MarshallSetupMethod.isEmpty()) {
                invokeMarshallSetupMethod(MarshallSetupMethod);
            }
        }
    }

    public void invokeMarshallSetupMethod(final String MarshallSetupMethod) {
        //classname
        int idx = MarshallSetupMethod.lastIndexOf(".");
        
        String cname = MarshallSetupMethod.substring(0, idx);
        String mname = MarshallSetupMethod.substring(idx);
        Class c = null;
        try {
            c = Class.forName(cname);
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
        
        java.lang.reflect.Method m = null;
        
        try {
            m = c.getMethod(mname);
        } catch (SecurityException ex) {
            ex.printStackTrace();
        } catch (NoSuchMethodException ex) {
            ex.printStackTrace();
        }
        
        /// FIXME: handle non static method!
        if (m != null) {
            Class[] p = m.getParameterTypes();
            if (p.length == 0) {
                if (p[0].toString() == "org.java.lang.Object") {
                    try {
                        m.invoke(this.MarshallObj);
                    } catch (IllegalArgumentException ex) {
                        ex.printStackTrace();
                    } catch (InvocationTargetException ex) {
                        ex.printStackTrace();
                    } catch (IllegalAccessException ex) {
                        ex.printStackTrace();
                    }
                }                 
            } else {
                System.out.println("Cannot setup marshaller. Setup method does not have org.java.lang.Object as the only param");
            }
        }
    }

    public java.lang.Object getMarshallObj() {
        return MarshallObj;
    }

    public void setMarshallObj(java.lang.Object MarshallObj) {
        this.MarshallObj = MarshallObj;
    }
    
    public String toString() {
//        if (this.value() != null)
//            return Converter.byteToHexString(this.value());
//        else
//            return new String("\0x00");
        byte[] value = this.value(false);
        
        String res = "\nDataBlock #" + this.internalID;
        res += "\nAlias: " + (this.Alias == null? "<none>": this.Alias);
        res += "\nDescription: " + (this.Alias == null? "<none>": this.Description);
        res += "\nBegin-Index: " + this.BeginIndex + "\nEnd-Index: " + this.EndIndex +
                "\nMarshall-Type: " + this.MarshallType + "\nBytes Value: " + 
                (value != null?value.length:0) + " byte(s)";
        if (value != null)
            res+=": " + Converter.byteToHexString(value);
        if (this.Parent != null)
            res+="\nParent: #" + this.Parent.getInternalID();
        res += "\nChildren: " + (children == null?0:children.size());
        if (children != null)
            for(int i=0;i<children.size();i++)
                res+="\nChildren #" + i + ": " + children.get(i).toString();
        
        return res;
    }
    
    public static DataBlock factory() {
        // this is just a simpel factory that differentiate the JIBX way and the reguler construction way
        return new DataBlock();
    }

    public void setParent(IDataBlock parent) {
        this.Parent = parent;
        this.updateByteSource();
    }

    public void setMarshallType(String marshallType) {
        this.MarshallType = marshallType;
    }
    
    public DataBlock() {
        // data block without any initialization to be use with factory and JIBX
        this.internalID = DataBlock.GlobalCounter++;
        this.log("Stripped DataBlock Constructor #" + this.internalID);
        this.BeginIndex = 0;
        this.EndIndex = 0;
        this.Parent = null;
        this.ByteSource = null;
        this.MarshallType = null;
        this.Props =  new HashMap<String, DataBlock>();
        this.cachedValueIsInitialized = false;
        this.cachedValue = null;
    }
    
//    public void presetEvent(Object obj) {
//        if (obj==null)
//            this.log("got NULL, maybe this is a root Object?");
//        else
//            this.log("casting obj as DataBlock: ID=" + ((ArrayList<DataBlock>) obj).size());
//    }
    
    public void postsetEvent(IUnmarshallingContext ctx) {
        this.log("PostSet in CtxDepth: " + ctx.getStackDepth());
        if (ctx.getStackDepth()<1) {
            this.log("This must be root Element");
            return;
        }
        
        DataBlock obj = (DataBlock) ctx.getStackObject(1);
        if (obj==null)
            this.log("postset: got NULL, maybe this is a root Object?");
        else
            this.log("postset: casting obj as DataBlock: ID=" + obj.getInternalID());
        
        this.log("PARENT: #" + obj.internalID);
        this.setParent(obj);
        
        // create marshaller
        this.log("Setup Marshaller: " + this.MarshallType);
        this.MarshallObj = this.marshallFactory(this.MarshallType);
        
        // append into aprent's hashmap
        obj.addChild(this.Alias, this);
    }
    
    private void log(String msg) {
//        if (this.LoggingEnabled)
            System.out.println("[DB #" + this.internalID + " - " + Alias + "] " + msg);
    }
    public int getInternalID() {
        return this.internalID;
    }

    public void setAlias(String alias) {
        this.Alias = alias;
    }
    
    private java.lang.Object marshallFactory(String marshallType) {
        Class mclass = null;
        try {
            mclass = Class.forName(marshallType);
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
        try {
            return mclass.newInstance();
        } catch (InstantiationException ex) {
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        }
        return null;
    }
    
    public void addChild(String alias, DataBlock datablock) {
        this.Props.put(alias, datablock);
    }
    
    public DataBlock getChild(String alias) {
        ///TODO: throws expception on null child!!
        return this.Props.get(alias);
    }
    
    public void enableLogging(boolean show) {
        this.LoggingEnabled = show;
    }
    
    
    /**
     * 
     * @return bytes from childs
     */
    
    public byte[] getCompoundValue() { 
       return getCompoundValue(false);
    }
    
    public byte[] getCompoundValue(boolean useCache) {
        if (this.Props.isEmpty())
            return value(useCache); // use cached value
        
        // temporary buffer
        ByteBuffer b = ByteBuffer.allocate(this.EndIndex - this.BeginIndex + 1);
        
        // result buffer
        ByteBuffer res = ByteBuffer.allocate(this.EndIndex - this.BeginIndex + 1);
        
        this.log("Allocating buffer: " + Converter.byteToHexString(b.array()) + " (" + b.capacity() + " bytes)");
        
        Collection<DataBlock> children = this.Props.values();

        Iterator<DataBlock> it = children.iterator();
        
        while (it.hasNext()) {
            b.rewind();
            res.rewind();
            DataBlock db = it.next();
            byte[] raw = db.getCompoundValue(useCache);
            
            if (raw == null)
                continue;
            
            this.log("Processing: " + db.Alias);
            
            // start part
            if (db.BeginIndex> 0) {
                this.log("bytes1 size: " + (db.BeginIndex));
                byte[] bytes1 = new byte[db.BeginIndex];
                b.get(bytes1, 0, bytes1.length);
                res.put(bytes1);
            }
            
            // main content
            b.position(db.BeginIndex);
            if (raw != null) {   
                this.log("b.position @" + b.position() + ", otherbyte: " + raw.length + " bytes, remaining: " + b.remaining() + " bytes");
                byte[] otherbyte = new byte[raw.length];
                b.get(otherbyte, 0, otherbyte.length);

                // real content
                for(int i=0;i<raw.length;i++)
                    raw[i] = (byte) ((raw[i] | otherbyte[i]) & 0x0000ff);

                // put real content
                this.log("re.remaining: " + res.remaining());
                res.put(raw, 0, raw.length);
            }
            
            // pad with zeros, necessarily
            int raw_length;
            if (raw == null) {
                raw_length = 0;
                //this.log("raw is NULL, will pad all bytes with 0x00");
            } else
                raw_length = raw.length;
                
//            this.log("re.remaining: " + res.remaining() + ", while want to pad:  " + ((db.EndIndex - db.BeginIndex + 1) - raw_length));
//            for(int i=1;i<(db.EndIndex - db.BeginIndex + 1) - raw_length;i++)
//                res.put((byte)0x0);
//            
            // end part, copy from EndIndex until last byte
            this.log("re.remaining: " + res.remaining() + ", while want to pad:  " + ((db.EndIndex - db.BeginIndex + 1) - raw_length));
            if (raw != null) {
                if (db.BeginIndex + raw.length - 1 < db.EndIndex) {
                    b.position(db.EndIndex);
                    byte[] bytes2 = new byte[b.remaining()];
                    b.get(bytes2, 0, bytes2.length);
                    res.put(bytes2);
                }
            }
            
            b.clear();
            b.put(res.array());
            
            //System.out.println("Processing children: " + db.Alias + " Content: " + Converter.byteToHexString(raw));
            if (raw != null)
                this.log("Replacing " + db.BeginIndex + " <--> " + db.EndIndex + " with " + Converter.byteToHexString(raw) + " (" + raw_length + " bytes)");
        }
        
        System.out.println("Returning Compound: " + b.capacity() + " bytes: " + Converter.byteToHexString(b.array()));
        return b.array();
    }
    
    /**
     * Explicitely write changes to bytesource
     */
    public void flush() {
        byte[] raw = this.getCompoundValue(true);
        
        // wrap bytesource. This will result in direct access onto the real array
        ByteBuffer b = ByteBuffer.wrap(this.ByteSource);        

        b.position(this.BeginIndex);
        b.put(raw);
    }
}
